5.3 select的底层实现及工作原理
select语句是Go语言中用于处理多个通道操作的一个强大工具,它能够在多个通道上同时进行非阻塞的选择操作。这对于实现并发程序的灵活性和复杂性处理非常有帮助。
本节我们将详细探讨select的内部实现及工作原理。
本节代码存放目录为 lesson15
select的底层实现
select的基本结构
select语句的实现涉及到以下几个核心部分:
通道的
case列表:select语句中每个case会对应一个通道操作,编译器会将这些case打包成一个select操作列表。case结构如下所示:type scase struct { c *hchan // chan elem unsafe.Pointer // data element }随机化:为了避免
select语句的饥饿问题(总是先处理某个case),Go语言的实现会对case列表进行随机化处理。阻塞队列:如果所有的通道都无法立即进行操作,
select语句会将当前的Goroutine加入到每个通道的等待队列中,并阻塞Goroutine,直到某个通道的操作可以进行。唤醒与继续:当某个通道的操作可以进行时,
select会唤醒相关的Goroutine,并继续执行与该通道关联的case。
select的操作流程
我们可以以下步骤理解select的操作流程:
初始化
select的case列表:编译器将每个case操作(通道的接收或发送)打包到一个列表中。随机化
case列表:为了避免饥饿,运行时会对这个case列表进行随机打乱,使得每次select的执行顺序都是随机的。遍历
case列表:对于每个
case,select语句会检查对应通道是否可以立即进行操作。如果可以,则直接执行该
case,并结束select语句。如果不可以,则将当前
Goroutine加入到该通道的等待队列中。
阻塞当前
Goroutine:如果所有的通道都不能立即操作,
select语句将阻塞当前的Goroutine,直到其中一个通道可以进行操作。当某个通道准备好后,该
Goroutine会被唤醒,执行与该通道关联的case。
默认情况
default:- 如果
select语句中存在default分支,并且所有通道都不能操作,那么select会立即执行default分支,而不会阻塞。
- 如果
我们可以通过下面的示意图来进行理解:
┌────────────────────────┐
│ select │
│ ┌───────────────────┐ │
│ │ case1: <- ch1 │ │
│ │ case2: <- ch2 │ │
│ │ case3: <- ch3 │ │
│ └───────────────────┘ │
└────────────────────────┘
│
▼
┌─────────────────────────┐
│ 运行时随机化 │
│ 随机打乱 case 列表 │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 顺序检查 case │
│ 检查 case1、case2... │
│ 按随机后的顺序 │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 执行一个可以操作的 case │
│ 例如:执行 case2 │
└─────────────────────────┘
│
▼
select 语句结束
select的实现原理
Go语言中的select语句依赖于调度器和通道的底层机制来实现。具体来说:
调度器:
select语句会与Go调度器紧密合作,当select阻塞时,调度器会将当前Goroutine挂起,并将其加入到通道的等待队列中。通道的队列:每个通道都有发送和接收的等待队列。当
select中的某个通道准备好时,通道的机制会从队列中唤醒对应的Goroutine。唤醒机制:当通道的状态发生变化时(例如一个通道的数据被接收或发送),通道会通过调度器唤醒阻塞在其上的
Goroutine,然后继续执行select语句的逻辑。
性能与使用建议
虽然select非常强大,但是在使用时也有一些性能和设计方面的考虑:
避免滥用
select:在高并发场景下,如果select语句处理的通道数量过多,可能会带来一些性能开销。使用
default分支:在某些情况下,添加default分支可以防止select语句永久阻塞,从而提高程序的响应性。关注
select的随机性:由于select语句的case选择是随机化的,因此不要依赖某个固定的选择顺序,这样可以避免一些难以调试的问题。
下面代码演示了一个常用的使用案例:
func main() {
ch1 := make(chan int64, 2)
ch2 := make(chan int64, 2)
ch3 := make(chan int64, 2)
wg.Add(1)
go func() {
defer wg.Done()
for {
ch1 <- time.Now().Unix()
time.Sleep(time.Duration(1) * time.Second)
ch2 <- time.Now().Unix()
time.Sleep(time.Duration(1) * time.Second)
ch3 <- time.Now().Unix()
time.Sleep(time.Duration(1) * time.Second)
}
}()
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case t1 := <-ch1:
fmt.Println("Received from ch1, ", t1)
case t2 := <-ch2:
fmt.Println("Received from ch2, ", t2)
case t3 := <-ch3:
fmt.Println("Received from ch3, ", t3)
}
}
}()
wg.Wait()
}
小结
select的主要作用就是用于对多个通道执行读取操作,这样一方面我们可以简化我们的程序,一方面我们也可以通过select执行一些流程操作。
select本质上就属于是监听了多个通道,所以我们不适合在select中使用大批量的case。